#!/bin/python
# ------------------------------------------------------------------------------
# By Kai Backman, Google - July 2006
#
# Include output parsing and analyzing code by Noel Llopis - April 2005
# http://www.gamesfromwithin.com
#
# This script convers an oo source directory into using precompiled headers

import os
import stat
import sys
import string
import os.path
import datetime
import re

class IncludeInfo:
    def __init__(self, name):
        self.name = name
    
    name = ""
    ocurrences = 1    
    ocurrences_directly = 0
    indent = 100
    includes = 0
    
    

    
def cleanup_path(path):
    path = path.lower()
    path = string.replace(path, "\\", "/")
    path = string.strip(path)
    return path
    

    
def ignore_file(inc, ignore_path):
    if ignore_path != "" and string.find(inc, ignore_path) != -1:
        return 1
        
    # Hack for gcc because it doesn't print full paths by default. 
    # Ignore anything without a / or starting with a . (relative path)
    if string.find(inc, "/") == -1 or inc[0] == ".":
        return 1
        
    return 0
    
    
def parse_include (indent, inc, ignore_path, current_info, include_map):
    
    # Update the number of includes caused by the current include (only the first time we encounter that include though).
    direct_include = (indent <= current_info.indent)
    
    if (not direct_include):
        if (current_info.ocurrences_directly == 1):
            current_info.includes += 1
                    
    if ignore_file(inc, ignore_path):
        #print "Skip", indent, inc
        current_info = IncludeInfo("")
        return current_info
        
    if include_map.has_key(inc):
        info = include_map[inc]
        info.ocurrences += 1
        if direct_include:
            info.ocurrences_directly += 1
        # print "Add", indent, inc
    else:
        info = IncludeInfo(inc)
        info.indent = indent
        if direct_include:
            info.ocurrences_directly += 1        
        include_map[inc]=info
        #print "New", indent, direct_include, inc
        
    if direct_include:
        current_info = info            
        #print "OK", indent, inc
        
    return current_info

    
def parse_includes(lines, ignore_path):
    
    include_map = {}
    
    include_regexp_vc  = re.compile('^Note\: including file\:(\s+)(.*)')
    include_regexp_gcc = re.compile('^(\.+) (.*)')
    current_info = IncludeInfo("")
    
    for line in lines:
        m = include_regexp_vc.search(line)
        if not m:
            m = include_regexp_gcc.search(line)
            
        if m:
            indent = len(m.group(1))-1
            inc = cleanup_path(m.group(2))
            current_info = parse_include(indent, inc, ignore_path, current_info, include_map)
                                    
    return include_map

    
def write_precomp_file(precomp_dir, precomp_basename, include_map, top_file_count ):
    precomp_filename = precomp_dir + "/" + precomp_basename + ".hxx"
    result = []
    for key in include_map.keys():
        if include_map[key].ocurrences_directly > 0:
            score = include_map[key].ocurrences * (include_map[key].includes+1)
            result.append(key)
       
    result.sort( lambda a,b: cmp(a[1], b[1]) )
    result.reverse()

    pathname, scriptname = os.path.split(sys.argv[0])
    header = file(pathname + "/header_copyleft.cxx", "r")
    boilerplate = header.read()
    header.close()
    precomp_data = boilerplate
    precomp_data += "// MARKER(update_precomp.py): Generated on " + str(datetime.datetime.today()) + "\n\n" + \
                    "#ifdef PRECOMPILED_HEADERS\n"

    # Process the actual include files to find the results we are interested in
    print "Using " + str(min(top_file_count, len(result))) + "/" + str(len(result)) + " header matches"
    for filename in xrange(min(top_file_count,len(result))):
        path = result[filename].split("/")
        if "solver" in path:
            subpath = path[path.index("solver") + 4:]
            if "stl" in subpath:
                # ignore internal stl headers
                if subpath[1] == "stl": continue
                precomp_data += "#include <" + "/".join(subpath[1:]) + ">\n"
            else:
                precomp_data += "#include <" + "/".join(subpath) + ">\n"
        elif "vc7" in path:
            subpath = path[path.index("vc7"):]
            subpath.reverse()
            subpath = subpath[:subpath.index("include")]
            subpath.reverse()
            precomp_data += "#include <" + "/".join(subpath) + ">\n"
        else:
            print "Ignoring path: " + "/".join(path)
            continue
    precomp_data += "#endif\n\n"
    fPrecomp = file(precomp_filename, "w")
    fPrecomp.write(precomp_data)
    fPrecomp.close()
    fCxxPrecomp = file(precomp_dir + "/" + precomp_basename + ".cxx", "w")
    fCxxPrecomp.write(boilerplate)
    fCxxPrecomp.write("#include \"" + precomp_basename + ".hxx\"\n\n")
    fCxxPrecomp.close()
    


# ------------------------------------------------------------------------------


def compile_showincludes(module):
    clean_command = "external/jam/jam clean-debug-" + module
    print module + ": Cleaning: " + clean_command
    clean = os.popen(clean_command)
    clean_output = clean.read()
    clean.close()
    build_command = "external/jam/jam -sCXXFLAGS=/showIncludes debug-" + module
    print module + ": Building: " + build_command
    build = os.popen(build_command)
    build_output = build.read()
    build.close()
    # Save the output just in case ..
    output = file("_update_precomp.log", "w")
    output.write(build_output)
    output.close()
    return build_output
    

# ------------------------------------------------------------------------------

def process_cxx(module):
    precomp_header_name = "precompiled_" + module + ".hxx"
    autogen_precomp_comment = "// MARKER(update_precomp.py): autogen include statement, do not remove"
    autogen_precomp_include = "#include \"" + precomp_header_name + "\""
    dirs_to_process = [module + "/source"]
    while len(dirs_to_process) > 0:
        dirs = dirs_to_process
        dirs_to_process = []
        for iDir in dirs:
            #print "Recursing: " + iDir
            files = os.listdir(iDir)
            for iFile in files:
                filename = iDir + "/" + iFile
                suffix = iFile[-4:]
                if filename[-4:] == ".svn" or filename[-2:] == ".." or filename[-1:] == "." :
                    continue
                elif stat.S_ISDIR(os.stat(filename).st_mode):
                    dirs_to_process.append(filename)
                elif suffix == ".cxx":
                    #print "Processing: " + filename
                    hFile = file(filename, 'r')
                    dataSource = hFile.read()
                    hFile.close()
                    preamble_end = dataSource.find("**/") + 4
                    if preamble_end == len(dataSource):
                        print filename + ":0: Error: Unable to find preamble end. File skipped"
                        continue
                    dataHeader = dataSource[:preamble_end]
                    dataBody = dataSource[preamble_end:]
                    bodyLines = dataBody.splitlines()

                    # Clean up old precomp directives and look for our autogen comment
                    ignore_ifdef = False
                    file_processed = False #If this file was already processed earlier
                    processedBody = ""
                    for line in bodyLines:
                        if ignore_ifdef:
                            if line.strip() == "#endif":
                                ignore_ifdef = False
                        elif line.strip() == "#ifdef PCH":
                            ignore_ifdef = True
                        elif line.strip() == "#pragma hdrstop":
                            continue
                        elif line.strip() == autogen_precomp_comment:
                            file_processed = True
                        else:
                            processedBody += line + "\n"
                    # Figure out if we want to ignore processing this file
                    if ignore_ifdef:
                        print filename + ":0: Error: pch ifdef still open at end of file"
                        continue
                    if file_processed:
                        continue
                    # Parse the fixed file together and write it out
                    dataProcessed = dataHeader + "\n" + \
                                    autogen_precomp_comment + "\n" + \
                                    autogen_precomp_include + "\n" + \
                                    processedBody
                    print "Writing: " + filename
                    fCXX = file(filename, "w")
                    fCXX.write(dataProcessed)
                    fCXX.close()

# ------------------------------------------------------------------------------
# Main

if __name__ == "__main__":
    if len(sys.argv) < 2 or len(sys.argv) > 3:
        print "Usage: " + sys.argv[0] + " module_name </showIncludes-output>"
#    if not os.path.isfile("external/jam/scripts/update_precomp.py"):
#        print "Error: Please run the script from the OO.o source root."
    else:
        module = sys.argv[1]
        build_output = ""
        # Read the compiler output from a file
        if len(sys.argv) == 3:
            print "Reading /showIncludes log from file: " + sys.argv[2]
            build_log = file(sys.argv[2], "r")
            build_output = build_log.read()
            build_log.close()
        # Or generate it by compiling the module
        else:
            build_output = compile_showincludes(module)
        
        print "Analyzing include data.."
        include_map = parse_includes(build_output.splitlines(), "")

        precomp_basename = "precompiled_" + module
        print "Writing precompiled include file: " + precomp_basename
        try:
            os.makedirs(module + "/inc/pch")
        except OSError:
            print "Directory already exists"
        write_precomp_file(module + "/inc/pch", precomp_basename, include_map, 2000)

        print "Adding include directives to cxx files.."
        process_cxx(module)
